package calao.storage.bdb;

import static calao.util.Logger.log;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Vector;

import com.sleepycat.bind.tuple.TupleOutput;
import com.sleepycat.bind.tuple.TupleInput;
import com.sleepycat.je.Database;

import calao.storage.Field;
import calao.storage.Table;
import calao.storage.Index;
import calao.storage.Sequence;
import calao.util.Printable;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;

class StorageRegistry 
implements Printable
{

	final static byte TABLE_DB_ID_SEQ_KEY = 1;

	final static byte SEQUENCE_KEY = 2;

	final static byte TABLE_KEY = 3;

	final static DatabaseConfig DB_CONFIG = new DatabaseConfig();

	static {
		DB_CONFIG.setAllowCreate(true);
	}

	final BdbStorage storage;
	final Database db;

	final HashMap<Field,Integer> fieldIdMap = new HashMap<Field,Integer>();
	final HashMap<Integer,Field> fieldMap = new HashMap<Integer,Field>();

	StorageRegistry(BdbStorage storage)
	throws DatabaseException
	{
		this.storage = storage;
		this.db = BdbStorage.env.openDatabase(null, 
				Integer.toString(storage.id)+".registry", DB_CONFIG);
	}

	private TableDbRecord toTableDbRecord(String tableName, DatabaseEntry data)
	{
		TupleInput in = Util.toInput(data);
		in.readPackedInt(); // encoding version
		int dbId = in.readPackedInt();
		Table table = new Table(tableName);
		int fieldCount = in.readPackedInt();
		for (int i=0; i<fieldCount; i++) {
			String fieldName = in.readString();
			final int valueType = in.readPackedInt();
			switch (valueType) {
			case 1:
				table.createField(fieldName, Field.INTEGER);
				break;
			case 2:
				table.createField(fieldName, Field.LONG);
				break;
			case 3:
				table.createField(fieldName, Field.DOUBLE);
				break;
			case 4:
				table.createField(fieldName, Field.STRING);
				break;
			case 5:
				try {
					Class<?> sclass = Class.forName(in.readString());
					table.createField(fieldName, sclass);
				}
				catch (ClassNotFoundException e) {
					throw new RuntimeException(e);
				}
				break;
			case 6:
				table.createField(fieldName, Field.SHORT);
				break;
			default:
				throw new Error();
			}
		}
		final int indexCount = in.readPackedInt();
		for (int i=0; i<indexCount; i++) {
			boolean isKey = in.readBoolean();
			int indexFieldCount = in.readPackedInt();
			Field[] fields = new Field[indexFieldCount];
			for (int j=0; j<indexFieldCount; j++) {
				fields[j] = table.getField(in.readString());
			}
			if (i==0) table.createPrimaryKeyIndex(fields);
			else if (isKey) table.createKeyIndex(fields);
			else table.createIndex(fields);
		}
		TableDbRecord rec = new TableDbRecord();
		rec.tableDef = table;
		rec.dbId = dbId;
		return rec;
	}

	TableDbRecord getTableDbRecord(String tableName)
	throws DatabaseException
	{
		DatabaseEntry data = Util.get(db, Util.toEntry(TABLE_KEY, tableName));
		return data!=null? toTableDbRecord(tableName, data) : null;
	}

	Iterable<TableDbRecord> getTableDbRecords()
	throws DatabaseException
	{
		Vector<TableDbRecord> result = new Vector<TableDbRecord>();
		Cursor cursor = db.openCursor(null, null);
		try {
			DatabaseEntry key = new DatabaseEntry();
			DatabaseEntry data = new DatabaseEntry();
			OperationStatus status = null;
			for (;;) {
				status = status==null? 
						cursor.getFirst(key, data, LockMode.DEFAULT)
						: cursor.getNext(key, data, LockMode.DEFAULT);
				if (status!=OperationStatus.SUCCESS) break;
				TupleInput in = Util.toInput(key);
				if (in.readByte()==TABLE_KEY) {
					result.add(toTableDbRecord(in.readString(), data));
				}
			}
			return result;
		}
		finally {
			cursor.close();
		}
	}

	void put(TableDbRecord record)
	throws DatabaseException
	{
		DatabaseEntry key = Util.toEntry(TABLE_KEY, record.tableDef.getName());
		TupleOutput data = new TupleOutput();
		data.writePackedInt(1); // encoding version
		data.writePackedInt(record.dbId);
		data.writePackedInt(record.tableDef.getFieldCount());
		for (Field field : record.tableDef.getFieldList()) {
			data.writeString(field.getName());
			switch (field.getType().toEnum()) {
			case INTEGER:
				data.writePackedInt(1);
				break;
			case LONG:
				data.writePackedInt(2);
				break;
			case DOUBLE:
				data.writePackedInt(3);
				break;
			case STRING:
				data.writePackedInt(4);
				break;
			case OBJECT:
				data.writePackedInt(5);
				data.writeString(field.getType().getValueType().getName());
				break;
			case SHORT:
				data.writePackedInt(6);
				break;
			default:
				throw new Error();
			}
		}
		data.writePackedInt(record.tableDef.getIndexCount());
		writeIndexDef(data, record.tableDef.getPrimaryKeyIndex());
		for (Index index : record.tableDef.getIndexList()) {
			if (index!=record.tableDef.getPrimaryKeyIndex()) {
				writeIndexDef(data, index);
			}
		}
		db.put(null, key, Util.toEntry(data));
	}
	
	private static void writeIndexDef(TupleOutput out, Index index)
	{
		out.writeBoolean(index.isKeyIndex());
		out.writePackedInt(index.getFieldCount());
		for (Field field : index) {
			out.writeString(field.getName());
		}		
	}

	synchronized int getNextSequenceValue(DatabaseEntry key, int min, int max) 
	throws DatabaseException
	{
		DatabaseEntry data = Util.get(db, key);
		int value;
		if (data!=null) {
			value = Util.toInt(data);
			if (value==max) value = min;
			else value++;
		}
		else value = min;
		db.put(null, key, Util.toEntry(value));
		return value;
	}

	synchronized int getNextValue(Sequence seq) 
	throws DatabaseException
	{
		return getNextSequenceValue(Util.toEntry(SEQUENCE_KEY, seq.getName()), 
				seq.getMinValue(), seq.getMaxValue());
	}

	void setValue(Sequence seq, int value)
	throws DatabaseException
	{
		DatabaseEntry key = Util.toEntry(SEQUENCE_KEY, seq.getName());
		db.put(null, key, Util.toEntry(value));		
	}

	int getValue(Sequence seq)
	throws DatabaseException
	{
		DatabaseEntry key = Util.toEntry(SEQUENCE_KEY, seq.getName());
		DatabaseEntry data = Util.get(db, key);
		return data!=null? Util.toInt(data) : seq.getMaxValue();
	}

	int newTableDbId()
	throws DatabaseException
	{
		return getNextSequenceValue(Util.toEntry(TABLE_DB_ID_SEQ_KEY), 
				1, Integer.MAX_VALUE);
	}
	
	void clear()
	throws DatabaseException
	{
		Cursor cursor = db.openCursor(null, null);
		try {
			DatabaseEntry key = new DatabaseEntry();
			DatabaseEntry data = new DatabaseEntry();
			data.setPartial(0, 0, true);
			OperationStatus status = null;
			for (;;) {
				status = status==null? 
						cursor.getFirst(key, data, LockMode.DEFAULT)
						: cursor.getNext(key, data, LockMode.DEFAULT);
				if (status!=OperationStatus.SUCCESS) break;
				cursor.delete();
			}
		}
		finally {
			cursor.close();
		}
	}

	public void printTo(PrintWriter writer) {
		Cursor cursor = null;
		try {
			cursor = db.openCursor(null, null);
			DatabaseEntry key = new DatabaseEntry();
			DatabaseEntry data = new DatabaseEntry();
			OperationStatus status = null;
			for (;;) {
				status = status==null? 
						cursor.getFirst(key, data, LockMode.DEFAULT)
						: cursor.getNext(key, data, LockMode.DEFAULT);
				if (status!=OperationStatus.SUCCESS) break;
				TupleInput in = Util.toInput(key);
				switch (in.readByte()) {
				case TABLE_KEY:
					writer.print("table ");
					toTableDbRecord(in.readString(), data).printTo(writer);
					break;
				case SEQUENCE_KEY:
					writer.format("sequence %s=%d", in.readString(), Util.toInt(data));
					break;
				default:
					continue;
				}
				writer.println();
			}
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
		finally {
			try {
				if (cursor!=null) cursor.close();
			}
			catch (DatabaseException e) {
				log(e);
			}
		}
	}

}
